package opencv;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.opencv.core.*;
import org.opencv.core.Point;
import org.opencv.features2d.*;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

import java.awt.*;
import java.awt.image.ImageObserver;
import java.awt.image.ImageProducer;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import static org.opencv.imgproc.Imgproc.*;

public class ImageRecognitionUtil {

    public static boolean matchImage(Mat templateImage, Mat originalImage) {
        MatOfKeyPoint templateKeyPoints = new MatOfKeyPoint();
        //指定特征点算法SURF
        FeatureDetector featureDetector = FeatureDetector.create(FeatureDetector.SURF);
        //获取模板图的特征点

        featureDetector.detect(templateImage, templateKeyPoints);
        //提取模板图的特征点
        MatOfKeyPoint templateDescriptors = new MatOfKeyPoint();
        DescriptorExtractor descriptorExtractor = DescriptorExtractor.create(DescriptorExtractor.SURF);
        System.out.println("提取模板图的特征点");
        descriptorExtractor.compute(templateImage, templateKeyPoints, templateDescriptors);

        //显示模板图的特征点图片
        Mat outputImage = new Mat(templateImage.rows(), templateImage.cols(), Imgcodecs.CV_LOAD_IMAGE_COLOR);
        System.out.println("在图片上显示提取的特征点");
        Features2d.drawKeypoints(templateImage, templateKeyPoints, outputImage, new Scalar(255, 0, 0), 0);

        //获取原图的特征点
        MatOfKeyPoint originalKeyPoints = new MatOfKeyPoint();
        MatOfKeyPoint originalDescriptors = new MatOfKeyPoint();
        featureDetector.detect(originalImage, originalKeyPoints);
        System.out.println("提取原图的特征点");
        descriptorExtractor.compute(originalImage, originalKeyPoints, originalDescriptors);

        List<MatOfDMatch> matches = new LinkedList();
        DescriptorMatcher descriptorMatcher = DescriptorMatcher.create(DescriptorMatcher.FLANNBASED);
        System.out.println("寻找最佳匹配");
        /**
         * knnMatch方法的作用就是在给定特征描述集合中寻找最佳匹配
         * 使用KNN-matching算法，令K=2，则每个match得到两个最接近的descriptor，然后计算最接近距离和次接近距离之间的比值，当比值大于既定值时，才作为最终match。
         */
        descriptorMatcher.knnMatch(templateDescriptors, originalDescriptors, matches, 2);

        System.out.println("计算匹配结果");
        LinkedList<DMatch> goodMatchesList = new LinkedList();
        //对匹配结果进行筛选，依据distance进行筛选
        matches.forEach(match -> {
            DMatch[] dmatcharray = match.toArray();
            DMatch m1 = dmatcharray[0];
            DMatch m2 = dmatcharray[1];
            if (m1.distance <= m2.distance * 0.7f) {
                goodMatchesList.addLast(m1);
            }
        });
        int matchesPointCount = goodMatchesList.size();
        //当匹配后的特征点大于等于 4 个，则认为模板图在原图中，该值可以自行调整
        System.out.println(matchesPointCount);
        return matchesPointCount >= 4;
    }




    private static final Log logger = LogFactory.getLog(ImageRecognitionUtil.class);

    private static MatOfPoint starContour;

    static {
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        URL url = ImageRecognitionUtil.class.getClassLoader().getResource("star.png");
        if (url == null) {
            logger.error("Template star image not found!");
        } else {
            String file = url.getPath().substring(1, url.getFile().length());
            Mat image = Imgcodecs.imread(file);
            Imgproc.cvtColor(image, image, Imgproc.COLOR_RGB2GRAY); // 灰度
            blur(image, image, new Size(3, 3)); // 模糊
            Canny(image, image, 3, 9, 3, true); // 提取边缘
            List<MatOfPoint> contours = new ArrayList<>();
            Imgproc.findContours(image, contours, new Mat(), RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
            starContour = contours.get(0);
        }
    }

    private ImageRecognitionUtil() {

    }

    private static final class StarLocationMatcher {

        private List<List<double[]>> list = new ArrayList<>();

        void add(MatOfPoint2f point2f) {
            Point[] points = point2f.toArray();
            double min = Double.MAX_VALUE;
            double max = 0;
            for (Point p : points) {
                min = Math.min(min, p.y);
                max = Math.max(max, p.y);
            }
            for (List<double[]> l : list) {
                if (!l.isEmpty()) {
                    double[] r = l.get(0);
                    if (matchRange(r[0], r[1], min, max)) {
                        l.add(new double[]{min, max});
                        return;
                    }
                }
            }
            List<double[]> l = new ArrayList<>();
            l.add(new double[]{min, max});
            list.add(l);
        }

        private static boolean matchRange(double minY1, double maxY1, double minY2, double maxY2) {
            double len = Math.abs(maxY1 - minY1);
            double error = len * 0.1;
            return Math.abs(minY1 - minY2) < error && Math.abs(maxY1 - maxY2) < error;
        }

        boolean matches() {
            int count = 0;
            for (List<double[]> l : list) {
                if (l.size() == 5) {
                    count++;
                }
            }
            return count == 3;
        }
    }

    public static boolean match5Stars(String fileName) {
        return calculate(Imgcodecs.imread(fileName));
    }


    private static boolean calculate(Mat image) {
        Imgproc.cvtColor(image, image, Imgproc.COLOR_RGB2GRAY);
        blur(image, image, new Size(3, 3));
        Canny(image, image, 3, 9, 3, true);
        List<MatOfPoint> contours = new ArrayList<>();
        Imgproc.findContours(image, contours, new Mat(), RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);

        MatOfPoint2f matOfPoint2f = new MatOfPoint2f();
        MatOfPoint2f approxCurve = new MatOfPoint2f();
        StarLocationMatcher matcher = new StarLocationMatcher();
        for (MatOfPoint contour : contours) {
            matOfPoint2f.fromList(contour.toList());
            Imgproc.approxPolyDP(matOfPoint2f, approxCurve, Imgproc.arcLength(matOfPoint2f, true) * 0.02, true); // 多边形检测
            long total = approxCurve.total();
            double match = matchShapes(contour, starContour, CV_CONTOURS_MATCH_I1, 0);
            if (total > 5 && total < 15 && match < 0.1) {
                matcher.add(approxCurve);
            }
        }
        return matcher.matches();
    }

    public static boolean match5Stars(InputStream inputStream) throws IOException {
        return calculate(readInputStreamIntoMat(inputStream));
    }

    private static Mat readInputStreamIntoMat(InputStream inputStream) throws IOException {
        // Read into byte-array
        byte[] temporaryImageInMemory = readStream(inputStream);

        // Decode into mat. Use any IMREAD_ option that describes your image appropriately
        return Imgcodecs.imdecode(new MatOfByte(temporaryImageInMemory), Imgcodecs.IMREAD_GRAYSCALE);
    }

    private static byte[] readStream(InputStream stream) throws IOException {
        // Copy content of the image to byte-array
        ByteArrayOutputStream buffer = new ByteArrayOutputStream();
        int nRead;
        byte[] data = new byte[16384];

        while ((nRead = stream.read(data, 0, data.length)) != -1) {
            buffer.write(data, 0, nRead);
        }

        buffer.flush();
        byte[] temporaryImageInMemory = buffer.toByteArray();
        buffer.close();
        stream.close();
        return temporaryImageInMemory;
    }
}
